Make rand_prototype dense for a sparse noise_rate_prototype - #3763
Merged
ChrisRackauckas merged 1 commit intoAug 1, 2026
Merged
Conversation
singhharsh1708
force-pushed
the
feat/sparse-noise-prototype
branch
2 times, most recently
from
June 23, 2026 19:23
f3ced09 to
f75eaab
Compare
singhharsh1708
force-pushed
the
feat/sparse-noise-prototype
branch
3 times, most recently
from
July 31, 2026 10:29
609f1a5 to
b327519
Compare
singhharsh1708
force-pushed
the
feat/sparse-noise-prototype
branch
from
July 31, 2026 20:09
b327519 to
6a53c3a
Compare
rand_prototype was built as `false .* noise_rate_prototype[1, :]`, so a sparse g handed the noise process a sparse Wiener increment. Sparsity in g says which states a channel drives, not which channels are idle, so every column still draws an increment and dW fills in on the first step. Two things go wrong from there. W2Ito1 sizes dZ dense no matter what dW is and stores both under one cache field type, so a sparse dW leaves W2Ito1Cache with no applicable constructor and the solve throws a MethodError. RKMilGeneral derives a long dZ from the prototype through `similar`, and sparse and dense arrays of that length take different randn! methods, so a sparse g quietly gave a different path than a dense g for the same seed. Fold the sparse noise_rate_prototype case into the existing issparse(u) branch, which already builds a dense increment and keeps the array type of u. Keeping the saved noise history dense also makes it cheaper: 400 states over 200 channels, EM at dt=1e-3 to t=10, goes from 136.71 MiB to 33.25 MiB and from 58 ms to 25 ms.
singhharsh1708
force-pushed
the
feat/sparse-noise-prototype
branch
from
August 1, 2026 21:26
6a53c3a to
7387459
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When an
SDEProblemis given a sparsenoise_rate_prototype,_sde_initbuilt the Wiener increment prototype asfalse .* noise_rate_prototype[1, :], which is aSparseVector. That is the wrong shape for the object: the sparsity pattern ofgrecords which states a channel drives, not which channels are idle, so every column still draws an increment anddWfills in completely on the first step.Most solvers cope with this, but two do not.
W2Ito1fails outright. Its cache keeps_dW,_dZandchi1under a singlerandTypefield, and builds_dZ = zeros(eltype(ΔW), 2)unconditionally. With a sparsedWthe three arguments have two different types and there is no applicable constructor:RKMilGeneralfails silently, which is worse. It derives its Lévy areadZfrom the increment prototype throughsimilar, so a sparse prototype gives a sparsedZof length 43 forp = 10and two noise channels.randn!has a specialized method forArray{Float64}and a genericAbstractArrayloop for everything else, and past a small length the two consume the RNG in a different order. Same problem, same seed, sparse versus densenoise_rate_prototype:The fix folds the sparse
noise_rate_prototypecase into the existingissparse(u)branch, which already builds a dense increment throughadapt(SciMLBase.parameterless_type(u), zeros(...)). Reusing that branch rather than a barezeroskeeps the array type ofu, so a GPUustill gets a device-resident increment.Keeping the increment dense is also cheaper, because the increment type is what the saved noise history stores. 400 states over 200 channels,
EMatdt = 1e-3out tot = 10:Testing
New file
lib/StochasticDiffEq/test/sparse_noise_tests.jl, registered inruntests.jl. Run against unmodified master it gives 17 passed, 3 failed, 1 errored; on this branch 21 passed. The four that move are the ones above:integrator.W.dWis aVector{Float64}rather than aSparseVector,W2Ito1reachesReturnCode.Successinstead of throwing, andRKMilGeneral(p = 10)produces the same trajectory from a sparse prototype as fromMatrix(noise_prototype)with the same seed. The remaining testsets coverEMandEulerHeunin place and out of place and pass either way.Also ran the nearby suites on the branch with no change:
nondiag_noise_eulerheun_test2/2,sparsediff_tests5/5,nondiagonal_tests5 passed 2 broken,noise_type_test2024/2024,scalar_noise1/1.This addresses part of #3178. That issue also asks for sparse noise processes that only generate the channels a problem actually uses, which this does not do, so I have not marked it closed.
Two adjacent instances of the same
false .* noise_rate_prototype[1, :]pattern are left alone, both in DelayDiffEq:ext/DelayDiffEqStochasticDiffEqCoreExt.jl:48andsrc/utils.jl:435(dW_dummy). They have the identical defect but sit in a different sublibrary, so they are out of scope here.AI Disclosure
Claude was used to help investigate the failure and draft this change.